//@author A0101119Y
#include "GCalMgr.h"

using namespace Twentyfifth;
using namespace std;

const string GCalMgr::GCALCMD_LOGIN = "login";
const string GCalMgr::GCALCMD_INFO = "info";
const string GCalMgr::GCALCMD_LOGOUT = "logout";
const string GCalMgr::GCALCMD_QUICKADD = "add";
const string GCalMgr::GCALCMD_SYNC = "sync";

const string GCalMgr::LABEL_PRIMARY = "primary";
const string GCalMgr::LABEL_ID = "id";
const string GCalMgr::LABEL_EMAIL_ADDRESS = "email";
const string GCalMgr::LABEL_TIME_ZONE = "timeZone";
const string GCalMgr::LABEL_CALENDAR_NAME_REMOTE = "summary";
const string GCalMgr::LABEL_CALENDAR_ID_REMOTE = "id";
const string GCalMgr::LABEL_CALENDAR_ID_LOCAL = "calendar_id";
const string GCalMgr::LABEL_TOKEN_EXPIRY = "expires_in";
const string GCalMgr::LABEL_LAST_UPDATED = "updated";

const string GCalMgr::VALUE_CALENDAR_NAME = "Twentyfifth";

const string GCalMgr::MSG_SUCCESS_SYNC = "Sync successful.";
const string GCalMgr::MSG_ERR_GENERAL = "GCalMgr error. Reason: ";
const string GCalMgr::MSG_ERR_INVALID_COMMAND = "Invalid command.";
const string GCalMgr::MSG_ERR_NO_STRING_FOR_ADD = "Invalid syntax for GCAL ADD. Please include a description.";
const string GCalMgr::MSG_ERR_CLEARED_ALL = "All credentials cleared. Please login again.";
const string GCalMgr::MSG_ERR_NOT_LOGGED_IN = "You must be logged in.";
const string GCalMgr::MSG_ERR_FULL_DAY_TASK = "The added task is a full-day event and is not supported by Twentyfifth.\nIt has, however, been added to your Google Calendar.";

Status GCalMgr::executeCommand(string input) {
	GCalCmd command = parse(input);
	Status status;
	try {
		switch (command.cmdtype) {
		case LOGIN:
			status = login();
			if (status.code == Status::OK) {
				executeOnLogin();
			}
			break;
		case INFO:
			status = info();
			break;
		case LOGOUT:
			status = logout();
			break;
		case QUICKADD:
			status = quickAdd(removeFirstWord(input));
			if (status.code == Status::OK) {
				syncDatabase(*databasePointer);
			}
			break;
		case SYNC:
			status = syncDatabase(*databasePointer);
			break;
		case INVALID:
		default:
			status.code = Status::ERR_INVALID_COMMAND;
			status.desc = MSG_ERR_INVALID_COMMAND;
			break;
		}
	}
	catch (Poco::Exception &e) {
		status.code = Status::ERR_OTHER;
		status.desc = e.displayText();
	}
	catch (exception&) {
		status.code = Status::ERR_OTHER;
	}
	return status;
}

GCalMgr::GCalCmd GCalMgr::parse(string input) {
	GCalCmd command;

	string cmdWord = getFirstWord(input);
	transform(cmdWord.begin(), cmdWord.end(), cmdWord.begin(), ::tolower);

	if (cmdWord == GCALCMD_LOGIN) {
		command.cmdtype = LOGIN;
	} else if (cmdWord == GCALCMD_INFO) {
		command.cmdtype = INFO;
	} else if (cmdWord == GCALCMD_QUICKADD) {
		command.cmdtype = QUICKADD;
	} else if (cmdWord == GCALCMD_SYNC) {
		command.cmdtype = SYNC;
	} else if (cmdWord == GCALCMD_LOGOUT) {
		command.cmdtype = LOGOUT;
	}

	return command;
}

//
// String manipulators.
// 

string GCalMgr::getFirstWord(string s) {
	int spacePosition = getSpaceAfterFirstWord(s);
	return s.substr(0, spacePosition);
}

string GCalMgr::removeFirstWord(string s) {
	int spacePosition = getSpaceAfterFirstWord(s);
	if (spacePosition == s.length()) {
		return BLANK_STRING;
	} else {
		return s.substr(spacePosition + 1, string::npos);
	}
}

int GCalMgr::getSpaceAfterFirstWord(string s) {
	int pos = 0;
	while (s[pos] != '\0') {
		if (s[pos] == ' ') return pos;
		pos++;
	}
	return pos;
}

//
// Commands for execution after login.
//

void GCalMgr::executeOnLogin() {

	try {
		getCredentials();
		linkToCalendar();
		//timeOffsetWithGoogle = getServerTimeOffset();
		syncDatabase(*databasePointer);
	}
	catch (Poco::Exception &e) {
		cerr << e.className() << ": " << e.displayText() << endl;
	}
	catch (exception &e) {
		cerr << e.what() << endl;
	}
}

void GCalMgr::getCredentials() {
	GCalVariableMgr varmgr;
	GCalRequestAgent requester;
	GCalParser parser;
	string responseJson = requester.getCalendars();
	vector<map<string, string>> calendarVector = parser.parseResponseToCalendars(responseJson);
	for (vector<map<string, string>>::iterator it = calendarVector.begin(); it != calendarVector.end(); it++) {
		try {
			string placeholder = (*it).at(LABEL_PRIMARY);
			varmgr.setValue(LABEL_EMAIL_ADDRESS, (*it).at(LABEL_ID));
			varmgr.setValue(LABEL_TIME_ZONE, (*it).at(LABEL_TIME_ZONE));
			break;
		}
		catch (exception&) {}
	}
	return;
}

void GCalMgr::linkToCalendar() {
	GCalVariableMgr varmgr;
	GCalRequestAgent requester;
	GCalParser parser;
	string responseJson = requester.getCalendars();
	vector<map<string, string>> calendarVector = parser.parseResponseToCalendars(responseJson);
	try {
		// First find calendar with name "Twentyfifth".
		string calendarId = getCalendarId(calendarVector);
		varmgr.setValue(LABEL_CALENDAR_ID_LOCAL, calendarId);
	}
	catch (Poco::Exception&) {
		// If it cannot be found, create one.
		string calendarId = requester.addCalendar(VALUE_CALENDAR_NAME);
		varmgr.setValue(LABEL_CALENDAR_ID_LOCAL, calendarId);
	}
}

string GCalMgr::getCalendarId(vector<map<string, string>> calendars) {
	for (vector<map<string, string>>::iterator it = calendars.begin(); it != calendars.end(); it++) {
		if ((*it).at(LABEL_CALENDAR_NAME_REMOTE) == VALUE_CALENDAR_NAME) {
			return (*it).at(LABEL_CALENDAR_ID_REMOTE);
		}
	}
	throw Poco::NotFoundException();
}

//
// Individual commands of GCalMgr.
//

Status GCalMgr::login() {
	Status s;
	GCalAuthHandler handler;
	s = handler.login();
	return s;
}

Status GCalMgr::info() {
	Status s;
	GCalAuthHandler handler;
	s = handler.info();
	return s;
}

Status GCalMgr::logout() {
	Status s;
	GCalAuthHandler handler;
	s = handler.logout();
	return s;
}

Status GCalMgr::quickAdd(string input) {
	Status s;
	if (!isLoggedIn()) {
		s.code = Status::ERR_OTHER;
		s.desc = MSG_ERR_NOT_LOGGED_IN;
		return s;
	}
	if (input == BLANK_STRING) {
		s.code = Status::ERR_INVALID_COMMAND;
		s.desc = MSG_ERR_NO_STRING_FOR_ADD;
		return s;
	}
	try {
		GCalVariableMgr varmgr;
		GCalRequestAgent requester;
		GCalParser parser;
		string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
		string response = requester.quickAddEvent(calendarId, input);
		Task t = parser.parseResponseToTask(response);
		s.tasks.push_back(t);
		s.code = Status::OK;
		if (t.isFullDay()) {
			s.desc = MSG_ERR_FULL_DAY_TASK;
		}
	}
	catch (Poco::Exception &e) {
		s.code = Status::ERR_OTHER;
		s.desc = e.displayText();
	}
	return s;
}

//
// Internal functions of GCalMgr.
//

bool GCalMgr::isLoggedIn() {
	GCalAuthHandler handler;
	return handler.isLoggedIn();
}

void GCalMgr::checkAndRenewAccessToken() {
	GCalVariableMgr varmgr;
	GCalParser parser;
	string expiryString = varmgr.getValue(LABEL_TOKEN_EXPIRY);
	DateTime expiryDt = parser.parseDtGoogleToTask(expiryString);
	DateTime nowDt;
	nowDt.setToNow();
	if (expiryDt <= nowDt) {
		try {
			GAccountsRequestAgent requestagent;
			requestagent.refreshAccessToken();
		}
		catch (Poco::Exception &e) {
			if (e.code() == 401) {
				varmgr.clearAll();
				cerr << MSG_ERR_CLEARED_ALL << endl;
				e.rethrow();
			}
		}
	}
}

void GCalMgr::setDbMgr(DbMgr& dbmgr) {
	databasePointer = &dbmgr;
}

//
// Synchronization functions.
//

Status GCalMgr::syncDatabase(DbMgr& dbmgr) {
	checkAndRenewAccessToken();
	assert(databasePointer != NULL);
	Status status;
	if (!isLoggedIn()) {
		status.code = Status::ERR_OTHER;
		status.desc = MSG_ERR_NOT_LOGGED_IN;
	}
	try {
		status = syncTimedTasks(dbmgr);
		status.code = Status::OK;
		status.desc = MSG_SUCCESS_SYNC;
	}
	catch (Poco::Exception &e) {
		status.code = Status::ERR_OTHER;
		status.desc = MSG_ERR_GENERAL + e.displayText();
	}
	return status;
}

string GCalMgr::getEventsFromRemote() {
	GCalVariableMgr varmgr;
	GCalRequestAgent requester;
	string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
	string response = requester.getEvents(calendarId);
	return response;
}

Status GCalMgr::syncTimedTasks(DbMgr& dbmgr) {
	Status s;
	vector<Task>& tasks = dbmgr.retrieveAllTasks();
	GCalVariableMgr varmgr;
	GCalParser parser;
	string responseJson;
	string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
	responseJson = getEventsFromRemote();
	vector<Task> remoteTasks = parser.parseResponseToTasks(responseJson);
	vector<bool> localTasksSettled, remoteTasksSettled;
	timeOffsetWithGoogle = getServerTimeOffset();

	if (!tasks.empty()) {
		localTasksSettled.resize(tasks.size(), false);
	}
	if (!remoteTasks.empty()) {
		remoteTasksSettled.resize(remoteTasks.size(), false);
	}

	for (int i = 0; i < tasks.size(); i++) {
		for (int j = 0; j < remoteTasks.size(); j++) {
			// 1. find matches by Google Calendar Event ID.
			if (tasks[i].getGCalId() == remoteTasks[j].getGCalId()) {
				syncTask(tasks[i], remoteTasks[j]);
				localTasksSettled[i] = true;
				remoteTasksSettled[j] = true;
				break;
			}
		}
		// 2. add tasks not found on remote server to Google.
		if (!localTasksSettled[i]) {
			if (!tasks[i].getDeleted()) {
				addTaskToRemote(tasks[i]);
				localTasksSettled[i] = true;
			}
		}
	}

	// 3. add tasks not found on local database from Google.
	for (int i = 0; i < remoteTasksSettled.size(); i++) {
		if (!remoteTasksSettled[i]) {
			if (!remoteTasks[i].getDeleted()) {
				if (!remoteTasks[i].isFullDay()) {
					dbmgr.addTask(remoteTasks[i]);
				}
			}
		}
	}
	dbmgr.writeAllToFile();
	return s;
}

void GCalMgr::syncTask(Task& localTask, Task remoteTask) {
	DateTime remoteLastUpdated = remoteTask.getLastUpdated();
	DateTime localLastUpdated = remoteLastUpdated;
	localLastUpdated.offset(timeOffsetWithGoogle);
	if (localTask.getDescription() == remoteTask.getDescription()) {
		if (localTask.getStart() == remoteTask.getStart()) {
			if (localTask.getEnd() == remoteTask.getEnd()) {
				if (localTask.getVenue() == remoteTask.getVenue()) {
					if (localTask.getDeleted() == remoteTask.getDeleted()) {
						// if complete match is found, do nothing.
						localTask.setLastUpdated(localLastUpdated);
						return;
					}
				}
			}
		}
	}
	if (localTask.getDeleted() && remoteTask.getDeleted()) {
		// if both sides are deleted, do nothing.
		return;
	}
	if (localTask.getDeleted()) {
		deleteTaskFromRemote(localTask);
		return;
	}
	if (remoteTask.getDeleted()) {
		localTask.setDeleted(true);
		return;
	}
	if (localLastUpdated < remoteLastUpdated) {
		updateLocalTask(localTask, remoteTask);
	} else {
		updateRemoteTask(localTask, remoteTask);
	}
}

void GCalMgr::updateLocalTask(Task& localTask, Task remoteTask) {
	DateTime remoteLastUpdated = remoteTask.getLastUpdated();
	DateTime localLastUpdated = remoteLastUpdated;
	localLastUpdated.offset(timeOffsetWithGoogle);
	localTask.setLastUpdated(localLastUpdated);
	if (remoteTask.getDeleted()) {
		localTask.setDeleted(true);
	} else {
		localTask.setDescription(remoteTask.getDescription());
		localTask.setVenue(remoteTask.getVenue());
		localTask.setStart(remoteTask.getStart());
		localTask.setEnd(remoteTask.getEnd());
	}
}

void GCalMgr::updateRemoteTask(Task& localTask, Task remoteTask) {
	GCalVariableMgr varmgr;
	GCalRequestAgent requester;
	string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
	if (localTask.getDeleted()) {
		requester.deleteEvent(calendarId, localTask);
	} else {
		requester.editEvent(calendarId, localTask);
	}
}

void GCalMgr::addTaskToRemote(Task& t) {
	if (!isLoggedIn()) {
		return;
	}
	if (t.getTaskType() != Task::TIMED_TASK) {
		return;
	}
	try {
		GCalRequestAgent agent;
		GCalVariableMgr varmgr;
		GCalParser parser;
		string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
		string responseEvent = agent.addEvent(calendarId, t);
		map<string, string> mapEvent = parser.parseJsonToMap(responseEvent);
		string eventId = mapEvent.at(LABEL_ID);
		string lastUpdated = mapEvent.at(LABEL_LAST_UPDATED);
		DateTime lastUpdatedDt = parser.parseDtGoogleToTask(lastUpdated);
		DateTime localLastUpdated = lastUpdatedDt;
		localLastUpdated.offset(timeOffsetWithGoogle);
		t.setGCalId(eventId);
		t.setLastUpdated(localLastUpdated);
		return;
	}
	catch (Poco::Exception &e) {
		cerr << MSG_ERR_GENERAL << e.displayText() << endl;
	}
}

void GCalMgr::deleteTaskFromRemote(Task& t) {
	if (!isLoggedIn()) {
		return;
	}
	if (t.getTaskType() != Task::TIMED_TASK) {
		return;
	}
	try {
		GCalRequestAgent agent;
		GCalVariableMgr varmgr;
		GCalParser parser;
		string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
		agent.deleteEvent(calendarId, t);
		return;
	}
	catch (Poco::Exception &e) {
		cerr << MSG_ERR_GENERAL << e.displayText() << endl;
	}
}

int GCalMgr::getServerTimeOffset() {
	if (!isLoggedIn()) {
		return 0;
	}
	GCalVariableMgr varmgr;
	string testEventString = VALUE_CALENDAR_NAME;
	Status testQuickAdd = quickAdd(testEventString);
	string calendarId = varmgr.getValue(LABEL_CALENDAR_ID_LOCAL);
	DateTime remoteTime = testQuickAdd.tasks[0].getLastUpdated();
	Task testEvent = testQuickAdd.tasks[0];
	GCalRequestAgent agent;
	agent.deleteEvent(calendarId, testEvent);
	DateTime localTime;
	localTime.setToNow();
	__time64_t remoteEpochTime = remoteTime.getUnixEpochTime();
	__time64_t localEpochTime = localTime.getUnixEpochTime();
	__time64_t timeDifference = localEpochTime - remoteEpochTime;
	return (int)timeDifference;
}